SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
7.6 KB · 131 lines tsx
Raw Blame History
1import Link from "next/link";2import type { Metadata } from "next";3import { notFound } from "next/navigation";4import { EventRow } from "@/components/event-row";5import { LiveFeed } from "@/components/live-feed";6import { Badge, Bar, Chip, Empty, PageHeader, Panel, Score, Sparkline, Table, Td, TierBadge } from "@/components/ui";7import { api } from "@/lib/api";8import { CHANNELS, CHANNEL_KEYS, feedHref, fmtInt, typeLabel } from "@/lib/format";910export const dynamic = "force-dynamic";1112/** Categories that are valid desks even when they have no recent event. */13const KNOWN = new Set<string>([...CHANNEL_KEYS, "cloud", "developer", "consumer-tech", "semiconductors", "pharma", "statistics", "space", "automotive", "commerce", "payments", "crypto", "enterprise", "internet", "standards", "technology", "media", "politics", "sports", "energy", "transport", "telecom", "education", "retail", "gaming", "web-policy", "packages", "api"]);1415function channelOf(channel: string) {16  return CHANNELS.find((c) => c.key === channel && c.key !== "all" && c.key !== "breaking" && c.key !== "silent");17}1819export async function generateMetadata({ params }: { params: Promise<{ channel: string }> }): Promise<Metadata> {20  const { channel } = await params;21  const label = channelOf(channel)?.label ?? channel;22  return { title: `${label} desk`, description: `Live ${label} events detected by WebSensor: breaking, silent changes, active sources and trending entities.`, alternates: { canonical: `/category/${channel}` } };23}2425/** Specialized real-time desk (spec §103). */26export default async function CategoryPage({ params }: { params: Promise<{ channel: string }> }) {27  const { channel } = await params;28  if (!/^[a-z0-9-]{2,32}$/.test(channel)) notFound();29  const desk = await api.categoryDesk(channel);30  if (!desk || (desk.recent.length === 0 && !KNOWN.has(channel))) notFound();31  const chan = channelOf(channel);32  const label = chan?.label ?? channel;33  const series = desk.series.map((p) => p.n);34  const total48 = series.reduce((a, b) => a + b, 0);35  const maxType = Math.max(1, ...desk.by_type.map((t) => t.n));36  const maxSrc = Math.max(1, ...desk.active_sources.map((s) => s.events_24h));37  return (38    <>39      <PageHeader40        compact41        kicker={42          <span className="flex flex-wrap items-center gap-1.5">43            <span>Desk</span>44            <span className="text-fg-subtle">·</span>45            {desk.categories.map((c) => (46              <Chip key={c} href={`/category/${c}`} tone={c === channel ? "signal" : "default"}>{c}</Chip>47            ))}48          </span>49        }50        title={label}51        description={`Meaningful changes from sources categorized as ${desk.categories.map((c) => `“${c}”`).join(", ")} — official pages, feeds, status pages, documentation, filings.`}52        actions={53          <div className="flex items-center gap-3">54            <div className="text-right">55              <div className="label">48 h</div>56              <div className="font-mono text-base font-semibold tabular">{fmtInt(total48)}</div>57            </div>58            <Sparkline values={series} width={160} height={36} tone="signal" />59          </div>60        }61      />62      <div className="grid gap-4 xl:grid-cols-[1fr_340px]">63        <div className="min-w-0">64          <LiveFeed initial={desk.recent} initialCursor={desk.nextCursor} fixed={chan ? chan.key : undefined} extraQuery={chan ? undefined : { category: channel }} showTabs={false} title={`${label.toUpperCase()} · LIVE`} />65        </div>66        <aside className="flex min-w-0 flex-col gap-4">67          <Panel title={<span className="text-hot">Breaking · 24 h</span>} dense action={<Link href={feedHref({ category: channel, signal_min: 80 }, "/live")} className="text-[11px] text-fg-subtle hover:text-fg">live →</Link>}>68            {desk.breaking.length ? desk.breaking.slice(0, 8).map((e) => <EventRow key={e.id} ev={e} />) : <Empty>Nothing is breaking in this desk right now.</Empty>}69          </Panel>70          <Panel title={<span className="text-silent">Silent</span>} dense action={<Link href={feedHref({ category: channel, silent_change: true }, "/live")} className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>71            {desk.silent.length ? desk.silent.slice(0, 8).map((e) => <EventRow key={e.id} ev={e} />) : <Empty>No silent change in this desk.</Empty>}72          </Panel>73          <Panel title="Active sources · 24 h" dense action={<Link href={`/sources?category=${channel}`} className="text-[11px] text-fg-subtle hover:text-fg">sources →</Link>}>74            {desk.active_sources.length ? (75              <Table head={["Source", "Kind", "24 h", "", "Max"]}>76                {desk.active_sources.map((s) => (77                  <tr key={s.id} className="hover:bg-panel-2/60">78                    <Td>79                      <span className="flex items-center gap-1.5"><TierBadge tier={s.tier} /><Link href={`/source/${s.id}`} className="truncate font-medium hover:underline">{s.name}</Link></span>80                      <div className="truncate font-mono text-[10.5px] text-fg-subtle">{s.domain}</div>81                    </Td>82                    <Td>{s.first_party === false ? <Badge kind="external" compact /> : <Badge kind="first-party" compact />}</Td>83                    <Td mono>{fmtInt(s.events_24h)}</Td>84                    <Td><div className="w-12"><Bar value={s.events_24h} max={maxSrc} /></div></Td>85                    <Td><Score value={s.max_importance} size="sm" /></Td>86                  </tr>87                ))}88              </Table>89            ) : (90              <Empty>No source produced an event in this desk in the last 24 h.</Empty>91            )}92          </Panel>93          <Panel title="Trending entities · 24 h" dense action={<Link href="/explore?tab=trending" className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>94            {desk.trending_entities.length ? (95              <ol className="divide-y divide-line">96                {desk.trending_entities.map((t, i) => (97                  <li key={t.id} className="grid grid-cols-[1.25rem_1fr_auto] items-center gap-2 px-3 py-1.5 text-[13px]">98                    <span className="font-mono text-[11px] text-fg-subtle tabular">{i + 1}</span>99                    <div className="min-w-0">100                      <Link href={`/entity/${t.id}`} className="block truncate font-medium hover:underline">{t.name}</Link>101                      <div className="truncate text-[11px] text-fg-subtle">{t.type.replace(/_/g, " ")} · {t.sources} source{t.sources === 1 ? "" : "s"}</div>102                    </div>103                    <span className="font-mono text-[12px] font-semibold tabular">{fmtInt(t.events_24h)}</span>104                  </li>105                ))}106              </ol>107            ) : (108              <Empty>No entity is trending in this desk.</Empty>109            )}110          </Panel>111          <Panel title="By type · 7 d" dense>112            {desk.by_type.length ? (113              <ul className="divide-y divide-line">114                {desk.by_type.map((t) => (115                  <li key={t.event_type} className="grid grid-cols-[8.5rem_1fr_3rem] items-center gap-2 px-3 py-1.5 text-[12.5px]">116                    <Link href={feedHref({ category: channel, event_type: t.event_type }, "/live")} className="truncate hover:underline">{typeLabel(t.event_type)}</Link>117                    <Bar value={t.n} max={maxType} tone="info" />118                    <span className="text-right font-mono text-fg-subtle tabular">{fmtInt(t.n)}</span>119                  </li>120                ))}121              </ul>122            ) : (123              <Empty />124            )}125          </Panel>126        </aside>127      </div>128    </>129  );130}131